;;; timefns-tests.el -- tests for timefns.c

;; Copyright (C) 2016-2018 Free Software Foundation, Inc.

;; This file is part of GNU Emacs.

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

(require 'ert)

;;; Check format-time-string and decode-time with various TZ settings.
;;; Use only POSIX-compatible TZ values, since the tests should work
;;; even if tzdb is not in use.
(ert-deftest format-time-string-with-zone ()
  ;; Don’t use (0 0 0 0) as the test case, as there are too many bugs
  ;; in MS-Windows (and presumably other) C libraries when formatting
  ;; time stamps near the Epoch of 1970-01-01 00:00:00 UTC, and this
  ;; test is for GNU Emacs, not for C runtimes.  Instead, look before
  ;; you leap: "look" is the timestamp just before the first leap
  ;; second on 1972-06-30 23:59:60 UTC, so it should format to the
  ;; same string regardless of whether the underlying C library
  ;; ignores leap seconds, while avoiding circa-1970 glitches.
  ;;
  ;; Similarly, stick to the limited set of time zones that are
  ;; supported by both POSIX and MS-Windows: exactly 3 ASCII letters
  ;; in the abbreviation, and no DST.
  (let ((format "%Y-%m-%d %H:%M:%S.%3N %z (%Z)"))
    (dolist (look '((1202 22527 999999 999999)
		    (7879679999900 . 100000)
		    (78796799999999999999 . 1000000000000)))
      ;; UTC.
      (should (string-equal
	       (format-time-string "%Y-%m-%d %H:%M:%S.%3N %z" look t)
	       "1972-06-30 23:59:59.999 +0000"))
      (should (equal (decode-time look t)
		     '(59 59 23 30 6 1972 5 nil 0)))
      ;; "UTC0".
      (should (string-equal
	       (format-time-string format look "UTC0")
	       "1972-06-30 23:59:59.999 +0000 (UTC)"))
      (should (equal (decode-time look "UTC0")
		     '(59 59 23 30 6 1972 5 nil 0)))
      ;; Negative UTC offset, as a Lisp list.
      (should (string-equal
	       (format-time-string format look '(-28800 "PST"))
	       "1972-06-30 15:59:59.999 -0800 (PST)"))
      (should (equal (decode-time look '(-28800 "PST"))
		     '(59 59 15 30 6 1972 5 nil -28800)))
      ;; Negative UTC offset, as a Lisp integer.
      (should (string-equal
	       (format-time-string format look -28800)
	       ;; MS-Windows build replaces unrecognizable TZ values,
	       ;; such as "-08", with "ZZZ".
	       (if (eq system-type 'windows-nt)
		   "1972-06-30 15:59:59.999 -0800 (ZZZ)"
		 "1972-06-30 15:59:59.999 -0800 (-08)")))
      (should (equal (decode-time look -28800)
		     '(59 59 15 30 6 1972 5 nil -28800)))
      ;; Positive UTC offset that is not an hour multiple, as a string.
      (should (string-equal
	       (format-time-string format look "IST-5:30")
	       "1972-07-01 05:29:59.999 +0530 (IST)"))
      (should (equal (decode-time look "IST-5:30")
		     '(59 29 5 1 7 1972 6 nil 19800))))))

(ert-deftest decode-then-encode-time ()
  (let ((time-values (list 0 -2 1 0.0 -0.0 -2.0 1.0
			   most-negative-fixnum most-positive-fixnum
			   (1- most-negative-fixnum)
			   (1+ most-positive-fixnum)
			   1e+INF -1e+INF 1e+NaN -1e+NaN
			   '(0 1 0 0) '(1 0 0 0) '(-1 0 0 0)
			   '(123456789000000 . 1000000)
			   (cons (1+ most-positive-fixnum) 1000000000000)
			   (cons 1000000000000 (1+ most-positive-fixnum)))))
    (dolist (a time-values)
      (let* ((d (ignore-errors (decode-time a t)))
	     (e (encode-time d))
	     (diff (float-time (time-subtract a e))))
	(should (or (not d)
		    (and (<= 0 diff) (< diff 1))))))))

;;; This should not dump core.
(ert-deftest format-time-string-with-outlandish-zone ()
  (should (stringp
           (format-time-string "%Y-%m-%d %H:%M:%S.%3N %z" nil
                               (concat (make-string 2048 ?X) "0")))))

(defun timefns-tests--have-leap-seconds ()
  (string-equal (format-time-string "%Y-%m-%d %H:%M:%S" 78796800 t)
                "1972-06-30 23:59:60"))

(ert-deftest format-time-string-with-bignum-on-32-bit ()
  (should (or (string-equal
               (format-time-string "%Y-%m-%d %H:%M:%S" (- (ash 1 31) 3600) t)
               "2038-01-19 02:14:08")
              (timefns-tests--have-leap-seconds))))

(ert-deftest time-equal-p-nil-nil ()
  (should (time-equal-p nil nil)))

(ert-deftest time-arith-tests ()
  (let ((time-values (list 0 -1 1 0.0 -0.0 -1.0 1.0
			   most-negative-fixnum most-positive-fixnum
			   (1- most-negative-fixnum)
			   (1+ most-positive-fixnum)
			   1e+INF -1e+INF 1e+NaN -1e+NaN
			   '(0 0 0 1) '(0 0 1 0) '(0 1 0 0) '(1 0 0 0)
			   '(-1 0 0 0) '(1 2 3 4) '(-1 2 3 4)
			   '(-123456789 . 100000) '(123456789 . 1000000)
			   (cons (1+ most-positive-fixnum) 1000000000000)
			   (cons 1000000000000 (1+ most-positive-fixnum)))))
    (dolist (a time-values)
      (dolist (b time-values)
	(let ((aa (time-subtract (time-add a b) b)))
	  (should (or (time-equal-p a aa) (and (floatp aa) (isnan aa)))))
	(should (= 1 (+ (if (time-less-p a b) 1 0)
			(if (time-equal-p a b) 1 0)
			(if (time-less-p b a) 1 0)
			(if (or (and (floatp a) (isnan a))
				(and (floatp b) (isnan b)))
			    1 0))))
	(should (or (not (time-less-p 0 b))
		    (time-less-p a (time-add a b))
		    (time-equal-p a (time-add a b))
		    (and (floatp (time-add a b)) (isnan (time-add a b)))))
	(let ((x (float-time (time-add a b)))
	      (y (+ (float-time a) (float-time b))))
	  (should (or (and (isnan x) (isnan y))
		      (= x y)
		      (< 0.99 (/ x y) 1.01)
		      (< 0.99 (/ (- (float-time a)) (float-time b))
			 1.01))))))))
